package com.googlecode.cswish.view;


import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.components.template.BaseTemplateEngine;
import org.apache.struts2.components.template.Template;
import org.apache.struts2.views.freemarker.FreemarkerManager;

import com.googlecode.cswish.model.PageElement;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.TextModel;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.struts.Action;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.conversion.AutoPageConverterManager;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.BlankObject;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;

import freemarker.template.Configuration;

/**
 * It's a generic Bean model to prepare the basic parameters for all kinds of template file 
 * <p>
 * 
 * According type parameter to call different tags to show the page element.
 * <p>
 * It support 2 kinds parameter source:
 * <li> Input from the <@cs.element/>
 * <li> Load from the pageElement object 
 * 
 * <p>
 * <li>Parameter Priority:<br>
 * Input parameter > Default parameter (from PageElement)<br><br>
 * <li>Parameter Extension:<br>
 * Input parameter support 'null' value to clean the default parameter<br><br>
 * <p>
 * <li>Functions Extension:<br>
 * Text model, Type <br><br>
 *  
 * @author Jerry.Feng Date: 2008-5-30
 */
public class Element extends UIBean {
	
	private static final Log logger = LogFactory.getLog(Element.class);
	
	private static final String TEXT_MODEL_FTL_FILE = "textmodel";
	
	// a sequence for the element in the same page. it's not thread safe
	private static int id = 0;
	
	// Store bean information to describe page data
	protected UIBean uibean;
	protected PageElement parentSetting;
	// Store the default UI parameter comes from PageConverterManager 
	protected PageElement defaultSetting;
	
	protected String textModel;
	
	// ftl template or another UIBean
	protected String type;
	// render a new PageElements according a object instance or class name 
	protected Object render;
	protected String renderMethod;
	protected Object value;
	protected String cascade;
	
	@Inject
	protected FreemarkerManager freemarkerManager;

	protected boolean devMode;
	
	protected ObjectFactory objectFactory;
    
	protected AutoPageConverterManager pageConverterManager;
	
	protected SafeManager safeManager;
	
	protected Functions functions;
    
    @Inject
    public void setObjectFactory(com.opensymphony.xwork2.ObjectFactory objectFactory) {
    	this.objectFactory = (ObjectFactory) objectFactory;
    	
    	pageConverterManager = this.objectFactory.getBean(AutoPageConverterManager.class, false);
    	safeManager = this.objectFactory.getBean(SafeManager.class, false);
    	functions = this.objectFactory.getBean(Functions.class, false);
    }
	
    public Element(ValueStack stack, HttpServletRequest request, HttpServletResponse response) {
        super(stack, request, response);
    }
	
	@Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        this.devMode = "true".equals(mode);
    }
    
	@Override
	public boolean start(Writer writer) {
		parentSetting = (PageElement) stack.getContext().get(FrameConstant.STACK_PAGEELEMENTS);
		defaultSetting = getDefaultSetting(parentSetting);
		
		// UIBean is found from PageElements
		if (type == null && defaultSetting != null) {
			type = defaultSetting.getType();
		}
		uibean = createUIBean();

		boolean continueProcessTag = true;
		try {
			pushSetting();
			
			// init default parameter according to uibean
			continueProcessTag = prepareInitParams();
			
			// check the input parameter and give the debug information
			if (logger.isInfoEnabled()) {
				debug();
			}
			
			if (continueProcessTag) {
				if (uibean != this) {
			        continueProcessTag = uibean.start(writer);
				} else {
					boolean hasCloseTag = type.endsWith("-close.ftl");
					
					if (hasCloseTag) {
						String openType = type.substring(0, type.length() - 10) + ".ftl";
						if (logger.isDebugEnabled()) {
							logger.debug("FREEMARKER TEMPLATE TRACE(object name--template name):" + name + " -- " + openType + '{');
						}
						try {
				            evaluateParams();
				            mergeTemplate(writer, buildTemplateName(openType, openType));
				        } catch (Exception e) {
				        	logger.error("Could not open template", e);
				        }
					}
					
					continueProcessTag = super.start(writer);
				}
			}
		} finally {
			if (!continueProcessTag) {
				popupSetting();
			}
		}
		return continueProcessTag;
    }
	
	@SuppressWarnings("unchecked")
	private void pushSetting() {
		stack.getContext().put(FrameConstant.STACK_PAGEELEMENTS, defaultSetting);
	}
	
	@SuppressWarnings("unchecked")
	private void popupSetting() {
		stack.getContext().put(FrameConstant.STACK_PAGEELEMENTS, parentSetting);
	}
	
	private void debug() {
		if (type == null) {
			logger.info("Can't find the element type, is it a null object? please provide the 'name' or 'type' for the null object");
		}
		
		if ("N/A".equals(name)) {
			// don't check the name
		} else if (name == null) {
			logger.info("No name is assigned to this element, is it only for display? " +
					"please provide a dummy 'name' to disable the informatio");
		}
		
		if (defaultSetting == null) {
			if (name != null) {
				if (type == null) {
					logger.info("No matched UI setting is found for element: " + name + ", please check if the parent element contains it");
				}
			} else {
				if (render == null) {
					logger.info("Please double check the 'render' value, it's null");
				} else {
					logger.info("No matched UI setting is found, render is invalid type? " +
							"PageElement, Class name, Object are optional for render");
				}
			}
			
			if (uibean == this) {
				Object value = parameters.get("value");
				if (value instanceof String) {
					String s = (String)value;
					if (s.length() > 1 && s.charAt(0) == '[' && s.charAt(s.length() - 1) == ']') {
						logger.info("wrong value? " + value);
					}
				} else if (value == null) {
					logger.info("no value? name=" + name);
				}
			}
		}
	}
	
	private UIBean createUIBean() {
		UIBean uibean;
		Class<? extends org.apache.struts2.components.UIBean> clazz = null;
		if (type == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Can't find the matched element type (element.name = " + name + ")");
			}
			uibean = this;
			type = "blank.ftl";
		} else {
			try {
				if (type != null && !type.endsWith(".ftl")) {
					String fullName = TagManager.getInstance().getTemplateName(type);
					if (fullName == null) {
						clazz = (Class<? extends UIBean>) TagManager.getInstance().getComponentClass(type);
						Constructor<? extends UIBean> constructor = clazz.getConstructor(ValueStack.class, HttpServletRequest.class, HttpServletResponse.class);
						uibean = constructor.newInstance(stack, request, response);
					} else {
						type = fullName;
						uibean = this;
					}
				} else {
					uibean = this;
				}
			} catch (Exception ex) {
				logger.error("Can't init element:" + type);
				uibean = this;
			}
		}
		
		uibean = (UIBean) objectFactory.autoWireBean(uibean);
		return uibean;
	}
	
	/**
	 * 1. load default setting from render object
	 * 2. try to create it again if the setting is on-time instance
	 * 
	 * @return
	 */
	private PageElement getDefaultSetting(PageElement parent) {
		PageElement defaultUISetting;
		
		if (render != null) {
			// defaultSetting = calculated by render
			if (render instanceof PageElement) {
				defaultUISetting = (PageElement) render;
			} else {
				// create the default UI setting
				if (renderMethod == null) {
					renderMethod = getDefaultMethod(parent);
				}
				if (render instanceof String && isClass((String)render)) {
					defaultUISetting = pageConverterManager.convertClass((String)render, renderMethod, null);
				} else {
					defaultUISetting = pageConverterManager.convertObject(render, renderMethod);
				}
				defaultUISetting.setParent(parent);
				
				if (parent != null) {
					if (isAvailableName(name)) {
						if (name.startsWith(parent.getName())) {	// full name
							defaultUISetting.setName(name);
						} else {									// short name
							defaultUISetting.setShortName(name);
						}
					}
				}
			}
		} else if (name == null) {
			defaultUISetting = parent;
		} else if (isAvailableName(name) && parent != null && parent instanceof PageElements) {
			defaultUISetting = ((PageElements)parent).getElement(name);
		} else {
			defaultUISetting = null;
		}

		if (defaultUISetting != null) {
			name = defaultUISetting.getName();
			
			boolean runtime = defaultUISetting.isParseOnRuntime();
			if (runtime) {
				Object value = null;
				if (!cleanPageValue(this.value)) {
					if (this.value != null) {
						value = this.value;
					} else {
						value = defaultUISetting.getValue();
					}
				}
				Object runtimeObj;
				if (value == null) {
					runtimeObj = stack.findValue(name);
				} else {
					runtimeObj = value;
				}
				defaultUISetting = pageConverterManager.convertObject(runtimeObj, getDefaultMethod(defaultUISetting));
				defaultUISetting.setParent(parent);
			}
		} 
		
		return defaultUISetting;
	}
	
	private boolean isClass(String name) {
		try {
			ClassUtils.getClass(name);
			return true;
		} catch (Throwable t) {
			return false;
		}
	}
	
	private boolean isAvailableName(String name) {
		return name != null && name.length() > 0 && !"N/A".equals(name);
	}
	
	private String getDefaultMethod(PageElement parent) {
		if (parent != null) {
			while (parent.getParent() != null) {
				parent = parent.getParent();
			}
		}
		String method;
		if (parent != null && parent instanceof PageElements) {
			PageElements top = (PageElements) parent;
			method = top.getMethod();
		} else {
			method = Invoker.DEFAULT_METHOD;
		}
		return method;
	}
	
	@Override
	public boolean end(Writer writer, String body) {
		boolean ret;
		
		try {
			if (uibean != this) {
				ret = uibean.end(writer, body);
				// the depth of "Element" stack is two
				super.popComponentStack();
				
				if (logger.isDebugEnabled()) {
					StringBuilder sb = new StringBuilder();
					sb.append("FREEMARKER TEMPLATE TRACE(object name--template name):").append(name).append(" -- ").append(uibean.getClass().getSimpleName());
					if (uibean.getTemplate() != null) {
						sb.append('(').append(uibean.getTemplate()).append(')');
					}
					logger.debug(sb.toString());
				}
			} else {
				ret = super.end(writer, body);
				
				if (logger.isDebugEnabled()) {
					logger.debug("} FREEMARKER TEMPLATE TRACE(object name--template name):" + name + " -- " + type);
				}
			}
		} finally {
			// restore old page structure
			popupSetting();
		}
		return ret;
	}
	
	protected Class getValueClassType() {
		return null; // don't convert nameValue to anything, we need the raw value
	}
	protected boolean evaluateNameValue() {
		// calculate the nameValue by current tag
        return false;
    }
	
	@SuppressWarnings("unchecked")
	public boolean prepareInitParams() {
		// theme
		if (cleanPageValue(super.theme)) {
			uibean.setTheme(null);
		} else {
			if (super.theme != null) {
				uibean.setTheme(super.theme);
			} else if (defaultSetting != null && defaultSetting.getTheme() != null) {
				uibean.setTheme(defaultSetting.getTheme());
			}
		}
		
		// required
		if (cleanPageValue(super.required)) {
			uibean.setRequired(null);
		} else {
			if (super.required != null) {
				uibean.setRequired(super.required);
			} else if (defaultSetting != null) {
				uibean.setRequired(String.valueOf(defaultSetting.isRequired()));
			}
		}

		// label
		if (cleanPageValue(super.label)) {
			uibean.setLabel(null);
		} else {
			if (super.label != null) {
				uibean.setLabel(super.label);
			} else if (defaultSetting != null && defaultSetting.getLabel() != null) {
				uibean.setLabel(getText(defaultSetting.getLabel()));
			}
		}
		
		// labelposition
		if (cleanPageValue(super.labelPosition)) {
			uibean.setLabelposition(null);
		} else {
			if (super.labelPosition != null) {
				uibean.setLabelposition(super.labelPosition);
			} else if (defaultSetting != null && defaultSetting.getLabelPosition() != null) {
				uibean.setLabelposition(defaultSetting.getLabelPosition());
			}
		}

		// name
		uibean.setName(name);
		
		// id
		if (cleanPageValue(super.id)) {
			uibean.setId(null);
		} else {
			if (super.id != null) {
				uibean.setId(super.id);
			} else if (defaultSetting != null && defaultSetting.getId() != null) {
				uibean.setId(defaultSetting.getId());
			}
		}
		
		// value (@see UIBean.evaluateParams, "if (parameters.containsKey("value"))" )
		if (cleanPageValue(this.value)) {
			uibean.getParameters().put("value", null);
		} else {
			// check the read permission
			if (this.value != null) {
				// the value is passed through the web page
				uibean.getParameters().put("value", this.value);
			} else if (defaultSetting != null && defaultSetting.getValue() != null) {
				uibean.getParameters().put("value", defaultSetting.getValue());
			} else if (uibean == this && name != null) {
				Object theValue = findValue(name);
				
				if (theValue != null && defaultSetting != null) {
					String checkFlag = (String) defaultSetting.getExtraParams().get(FrameConstant.SEC_RUNTIME_CHECK);
					
					if (checkFlag != null) {
						int index = name.lastIndexOf('.');
						String parentModelName = name.substring(0, index);
						String fieldName = name.substring(index + 1);
						
						Object model = findValue(parentModelName);
						String userId = safeManager.getLoginUser(request).getLoginId();
						
						String path = functions.linkModelNoBase(model.getClass().getName(), Action.VIEW);
						
						if ("R".equals(checkFlag)) {
							String ret = safeManager.checkReadField(userId, path, new Context(stack), true, fieldName);
							if (ret != null) {
								theValue = ret;
							}
						} else {
							safeManager.checkWriteField(userId, path, new Context(stack), true, fieldName);
						}
						
						
					}// check permission
				}
				parameters.put("value", theValue);
			}			
		}
		
		// text model
		if (this.textModel != null) {
			uibean.addParameter("textModel", this.textModel);
		} else if (defaultSetting != null && defaultSetting.getTextModel() != null) {
			uibean.addParameter("textModel", defaultSetting.getTextModel());
		}
		
		// length
		if (defaultSetting != null && defaultSetting.getLength() > 0) {
			uibean.getParameters().put("length", defaultSetting.getLength());
		}
		
		// extra parameters
		if (defaultSetting != null && defaultSetting.getExtraParams(false) != null) {
			uibean.getParameters().putAll(defaultSetting.getExtraParams(false));
		}
		Map inputParameters = this.getParameters();
		if (inputParameters != null && inputParameters.size() > 0) {
			uibean.getParameters().putAll(inputParameters);
		}
		Object oTextModel = uibean.getParameters().get("textModel");
		TextModel theTextModel = TextModel.toTextModel(oTextModel);
		uibean.setTemplate(getRealTemplate(theTextModel));
		
		return true;
	}
	
	private boolean cleanPageValue(Object valueFromPage) {
		return ("null".equals(valueFromPage));
	}

	@Override
	protected String getDefaultTemplate() {
		if (uibean != this) {
			return "element";
		} else {
			return type;
		}
	}
	@Override
	public String getTemplateDir() {
		if (uibean != this || type.charAt(0) != '/') {
			return super.getTemplateDir();
		} else {
			return "";
		}
	}
	
	private String getText(String label) {
		String text = stack.findString("getText('" + label + "')");
		return text == null ? label : text;
	}
	
	private String getRealTemplate(TextModel textModel) {
		if (textModel == TextModel.FALSE 
				|| !uibean.getClass().getName().startsWith(TagManager.STRUTS_COMPONENT_PACKAGE)) {
			return uibean.getTemplate();
		} else {
			// FIXME: is there better way to get the template name? use cache?
			// @see FreemarkerTemplateEngine.renderTemplate
			if (TextModel.TRUE_WITH_VALUE == textModel) {
		        try {
		        	Method defaultTemplateMethod = uibean.getClass().getDeclaredMethod("getDefaultTemplate", BlankObject.CLassArray);
		        	defaultTemplateMethod.setAccessible(true);
		        	Object[] args = null;
					String defaultTemplate = (String) defaultTemplateMethod.invoke(uibean, args);
					String theme = getTheme();
					setTheme("simple");
					Template template = buildTemplateName(uibean.getTemplate(), defaultTemplate);
					setTheme(theme);
					final BaseTemplateEngine engine = (BaseTemplateEngine) templateEngineManager.getTemplateEngine(template, templateSuffix);
					Map context = stack.getContext();
			        ServletContext servletContext = (ServletContext) context.get(ServletActionContext.SERVLET_CONTEXT);
			        
					Configuration config = freemarkerManager.getConfiguration(servletContext);
					
					List<Template> templates = template.getPossibleTemplates(engine);
					String templateName = null;
					for (Template t : templates) {
						templateName = t.toString() + '.' + "ftl";
			            try {
		                    // try to load, and if it works, stop at the first one
		                    config.getTemplate(templateName);
		                    break;
		                } catch (IOException e) {
		                	// skip the exception
		                }
					}
					uibean.getParameters().put("templateName", templateName);
		        } catch (Exception ex) {
		        	logger.error("can't find the corrent template name", ex);
		        }
			}
			
			return TEXT_MODEL_FTL_FILE;
		}
	}

	public String getTextModel() {
		return textModel;
	}

	public void setTextModel(String textModel) {
		this.textModel = textModel;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getCascade() {
		return cascade;
	}

	public void setCascade(String cascade) {
		this.cascade = cascade;
	}

	public Object getRender() {
		return render;
	}

	public void setRender(Object render) {
		this.render = render;
	}

	public String getRenderMethod() {
		return renderMethod;
	}

	public void setRenderMethod(String renderMethod) {
		this.renderMethod = renderMethod;
	}

	public Object getValue() {
		return value;
	}

	public void setValue(Object value) {
		this.value = value;
	}
	
	public void setParameters(Map parameters) {
		this.parameters = parameters;
	}
	
	public String toString() {
		return this.name;
	}
}